DSL(Domain Specific Language)

#Java #Kotlin #DSL #Kotlin-Scope_Function #Kotlin-Lambda_With_Receiver #Kotlin-수신객체지정람다

1. DSL?

어떤 곳에서는 SQL이 SDL이라고 하고, HTML이나 CSS도 DSL이라고 한다.
그럼 자바의 DSL, 코틀린의 DSL은 무엇인가? SQL, HTML, CSS는 자체로 언어같은 느낌이다. (프로그래밍 언어가 아닐뿐 언어는 언어다.) 그럼 자바의 DSL, 코틀린의 DSL은 *언어 언어 같은건가?

DSL은 특정 도메인을 위한 언어이다. SQL은 데이터베이스 도메인을 위한 언어이고, CSS는 웹 애플리케이션 스타일 도메인을 위한 언어이다([DSL - CSS]). 반면에 C언어나 코틀린은 모든 도메인에서 사용할 수 있는 범용 언어(GPL-General Programming Language)이다. (UI, 미디어, 금융, 전자상거래등의 수많은 도메인을 표현하기위해 사용가능하다.)

그럼 아래에서 *언어의 언어의 의미를 알아보자.

2-1. 외부 DSL, 내부 DSL?

한글의 자음과 모음을 사용해서 사용하고 있는 나라는 우리나라와 찌아찌아족이라는 인도네시아 부족이 있다. 하지만 찌아찌아족은 그들의 고유한 구어(口語)가 있고, 그 구어를 한글로 표현한다고 한다.
즉, 그들의 생활 도메인을 한글을 통해서 표현하고 있는 것이다. (참고)
|400

비유로 소개한 찌아찌아족의 예를 통해 내부 DSL을 설명해보았다.(한글 고유의 문법이라는 측면에서는 다소 부족한 사례일 수 있으니 영감만 가져가실 수 있으면 좋을 것 같다.) 이처럼 내부 DSL은 주 언어의 형식을 빌려 특정 도메인에서 사용될 수 있는 체계로 확장 설계된 언어라고 할 수 있다.

예를들어, 안드로이드 그레이들 빌드 구성 파일의 기본 언어가 된 코틀린 DSL을 볼 수 있을 것 같다. 이때의 코틀린DSL의 도메인은 그레이들 빌드이고, API는 제공되어 있다. 제공된 API를 보면 문법을 알 수 있고, 이 API가 어떻게 만들어졌는지를 보면 코틀린의 함수형태를 사용한다. 다른 예들은, Ktor프레임워크의 HTML, CSS DSL이 있고, 쿼리와 관련된 처리를 위한 kotlin-jdsl 같은 것들도 있다.

반면 SQL, HTML, CSS 처럼 이것을 파싱하고 컴파일하는 주 언어와는 아예 다른 문법 구조를 가지고 있는 언어를 외부 DSL이라고 한다.


2. DSL 구성? 생성? 제작? 방법과 사용방법

DSL을 만든다는 것을 구성한다고 해야할지, 생성한다고 해야할지, 제작한다고 해야할지 모르겠다.
DSL을 만들기 위해 코드의 디자인은 몇가지 패턴을 따르게 된다. 이것을 알아보자

2-1. Java

모던 자바 - DSL(도메인 전용 언어)이란?, beekei
beekei님이 패턴 몇가지를 자세한 예를 들어 설명해 놓은 포스트가 있다. 생성 방식과 사용 방식은 beekei님 포스트를 참고하고 이 글에서는 정리만 하고자 한다.

메서드 체인

Builder로 DSL을 구성하여 필요한 메서드들을 체이닝형식으로 호출하며 구문을 이루는 DSL 이다.
구문이 일렬로 늘어진다는 특징이 있다. (의미를 정확하게 드러내기 위해서는 알맞은 곳에서 끊어주고 들여써줘야 한다.)

중첩 함수

함수 안에 함수를 사용하는 방식으로 구문을 이루는 DSL이다.
함수안의 함수안의 함수.... 로 되어 있어서 구문이 계층을 이룰 수 있게 된다.
또 다른 특징으로는 인자 부분에서 함수를 호출하는데(함수를 인자로 넘겨주는 것이 아니라 함수를 호출하고 그 리턴값이 인자로 들어간다.) 이렇게 하는 이유는 메서드 명이 구문의 의미 요소가 되기 때문이다. public static Product product(Product product) { return product; }와 같은 것을 보면 이 함수를 호출하는 것이 로직적으로는 아무 의미가 없다는 것을 알 수 있다. 하지만 인수에 product()메서드를 호출하면서 product라는 구문의 단어로서 의미를 가지게 된다.
(코틀린의 경우엔 order(coupon = coupon, product = product)라고 표현해 줄 수 있기 때문에 이런 패턴은 살짝 달라질 수도 있을 것 같다.)

람다

플루언트 방식을 사용하게 된다. (고 하셨는데 이 부분에 대해서는 내가 아직 이해가 부족한 것 같다.) 계층구조를 잘 볼 수 있다는 특징은 중첩함수와 같다.

2-2. Kotlin

기타 참고1. TEAM-ASC/Chapter11.DSL 만들기/DSL.md

Chat-GPT에 물어봐서 얻은 답변이다.
Java의 패턴이랑 같은 원리를 이용해서 만들어지는 것으로 확인해봤을 때 적절한 답변으로 판단되어 첨부한다.

답변 시작 -----------------------------------------------------------

코틀린에서 DSL을 만드는 방법은 다양하지만, 주로 사용되는 방식 중 몇 가지를 소개하겠습니다. DSL을 만드는 기본적인 접근 방법은 함수 리터럴이나 확장 함수, 람다 등을 활용하여 코틀린의 풍부한 함수형 프로그래밍 기능을 활용하는 것입니다.

1. 수신 객체 지정 람다 (Receiver Object Lambda)

수신 객체 지정 람다는 특정 객체의 범위에서 함수를 호출할 수 있는 람다식입니다. 이를 이용하여 DSL을 만들 때 특정 객체의 함수를 DSL 내에서 자연스럽게 호출할 수 있습니다.

class Configuration {
    var host: String = ""
    var port: Int = 0
}

fun configure(block: Configuration.() -> Unit): Configuration {
    val config = Configuration()
    config.block() // 수신 객체 지정 람다 호출
    return config
}

// DSL 사용 예
val myConfig = configure {
    host = "example.com"
    port = 8080
}

위의 예제에서 configure 함수는 Configuration 객체를 생성하고, block 함수 리터럴을 통해 객체의 멤버 변수를 설정할 수 있습니다. 이를 통해 DSL의 형태를 갖추게 됩니다.

2. 확장 함수를 이용한 DSL

코틀린의 확장 함수를 사용하여 기존 클래스나 인터페이스에 DSL 형태의 새로운 기능을 추가할 수 있습니다.

class Document {
    var title: String = ""
    var body: String = ""
}

fun Document.header(init: Header.() -> Unit) {
    val header = Header()
    header.init()
    this.title = header.title
}

class Header {
    var title: String = ""
}

// DSL 사용 예
val myDocument = Document().apply {
    header {
        title = "Sample Document"
    }
    body = "This is the body of the document."
}

위의 예제에서 Document 클래스에 header 확장 함수를 정의하여 DSL 형태로 제목을 설정할 수 있습니다. header 함수 내에서는 Header 객체를 생성하고 초기화한 후, Document 객체의 필드에 설정하는 방식으로 동작합니다.

3. Builder 패턴을 이용한 DSL

빌더 패턴을 코틀린에서 구현하여 DSL을 만들 수도 있습니다. 이 경우에는 일반적으로 빌더 클래스를 정의하고 그 안에서 DSL의 형태를 제공합니다.

class Person {
    var name: String = ""
    var age: Int = 0
}

class PersonBuilder {
    var person = Person()

    fun name(name: String) {
        person.name = name
    }

    fun age(age: Int) {
        person.age = age
    }
}

fun person(block: PersonBuilder.() -> Unit): Person {
    val builder = PersonBuilder()
    builder.block()
    return builder.person
}

// DSL 사용 예
val person = person {
    name("John Doe")
    age(30)
}

위의 예제에서는 Person 클래스와 PersonBuilder 클래스를 정의하고, person 함수를 통해 DSL 형태로 Person 객체를 생성할 수 있습니다. PersonBuilder 클래스에서는 DSL을 통해 설정된 값들을 Person 객체에 저장합니다.

사용 방법

DSL을 사용할 때는 주로 람다식을 이용하여 DSL 블록을 정의하고, 해당 블록 내에서 DSL의 구조를 설계합니다. DSL을 사용하는 측에서는 람다 내에서 제공된 확장 함수나 빌더 함수 등을 통해 DSL을 완성시킵니다.

이러한 방식으로 코틀린에서는 다양한 DSL을 만들 수 있으며, 각 DSL이 다른 문제 영역이나 도메인에서의 사용성을 높일 수 있습니다.

답변 끝 -----------------------------------------------------------